注意:所有文章除特别说明外,转载请注明出处.
HashMap源码解析
[TOC]
1. 默认属性
1 | static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //默认初始容量 16 |
2. put方法
1 | public V put(K key, V value) { |
hash方法
1 | static final int hash(Object key) { |
==putval(添加)方法==
1 | final V putVal(int hash, K key, V value, boolean onlyIfAbsent, |
==PUT方法的流程:==
- 1、如果未被初始化,则初始化
- 2、根据KEY求Hash值,然后计算下标。
- 3、如果没有碰撞直接放入bucket中。
- 4、如果碰撞了,则以链表的方式连接到后面。
- 5、如果链表长度大于 8,则调转换为红黑树的方法
- 6、如果树节点个数低于 6,则调转换链表的方法
- 7、如果节点已经存在则直接替换
- 8、如果超过了阈值,则进行扩容
resize(扩容)方法
1 | //初始化或者扩容之后元素调整 |
split(红黑树修剪)方法
1 | 上述方法在resize()过程中被调用 |
untreeify(非树化)方法
1 | 如果链表的长度 <= UNTREEIFY_THRESHOLD(6) 就进行非树化,否则就进行树化。 |
treeifyBin(树化)方法
1 | treeifyBin方法,应该可以解释为:把容器里的元素变成树结构 |
treeify(打头树化)方法
1 |
|
moveRootToFront(保证根节点)方法
1 | /** |
putTreeVal(树的添加节点)方法
当同一个位置上链表中的元素达到8个的时候,就会再将这些元素构建成一个红黑树(参见:treeifyBin方法分析),同时把==原来的单链表结构变成了双链表结构==,也就是这些==元素即维持着红黑树的结构又维持着双链表的结构==。当第9个相同hash值的键值对put过来时,发现该位置已经是一个树节点了,那么就会调用putTreeVal方法,将这个新的值设置到指定的key上。
1 |
|
balanceInsertion(树的平衡)方法
1 | 红黑树插入节点后,需要重新平衡 |
ConcurrentHashMap
1、简介
ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现。==ConcurrentHashMap不允许key或value为null值==。
2.为什么HashMap用于多线程会出错?:
JDK1.7 的 HashMap在高并发环境下会形成环状链表,导致get操作时,进入死循环,所以,在并发环境中使用HashMap是非常危险的。
3.为什么不用Hashtable?
HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
4、ConcurrentHashMap java7 中的实现方式
HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的==”分段锁”==思想。
4.1 java7中的优缺点
ConcurrentHashMap采用了非常精妙的”分段锁”策略,ConcurrentHashMap的主干是个==Segment==数组。每个段其实==就是一个小的Hashtable==,它们有自己的锁。==只要多个修改操作发生在不同的段上,它们就可以并发进行。(JDK7中是这样实现的)==
==但是!!!==有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。这里“按顺序”是很重要的,否则极有可能出现死锁.
所以,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。
ConcurrentHashMap java8 中的实现
1、简介
JDK8:ConcurrentHashMap在JDK8中进行了巨大改动,它摒弃了Segment(锁段)的概念,而是启用了一种全新的方式实现,利用==CAS==算法。
2、什么是volatille关键字?
volatile是一种轻量级的同步机制,它主要有两个特性:
一是保证共享变量对所有线程的可见性;(==内存可见性==)
二是禁止指令重排序优化。同时需要注意的是,volatile对于单个的共享变量的读/写具有原子性,但是像num++这种复合操作(读写存),volatile无法保证其原子性。
==3、重要属性==
3.1 sizeCtl
可以说它是ConcurrentHashMap中出镜率很高的一个属性,因为它是一个==控制标识符==,在不同的地方有不同用途,而且它的取值不同,也代表不同的含义。
- 负数代表正在进行初始化或扩容操作
- -1代表正在初始化
- -N 表示有N-1个线程正在进行扩容操作
- 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,==这一点类似于扩容阈值的概念==。还后面可以看到,它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的
3.2初始化数组 initTable
初始化方法主要应用了关键属性sizeCtl 如果这个值〈0,表示其他线程正在进行初始化,就放弃这个操作。在这也可以看出ConcurrentHashMap的==初始化只能由一个线程完成==。如果获得了初始化权限,就用CAS方法将sizeCtl置为-1,防止其他线程进入。初始化数组后,将sizeCtl的值改为0.75*n。
3.3 核心内容
- forword ((transfer)扩容时标记,碰到这个标记直接跳过),==类似于大家一起搭积木==
- Moved (在put时候碰到Moved ,则帮助其扩容 helptransfer)
- sizeCtl
- CAS
- Volatile(val 和 next 都用了)
- 以上五点保证了 ConcurrentHashMap 的高并发情况下的线程安全问题
- java7中只有1000多行代码,而在Java8中重新编写了现在有6000多行代码
==PUT流程:==
- 1、判断是否初始化过,没有则进行初始化。
- 2、通过Hash定位到数组的索引坐标,判断是否有Node 节点,
- 如果没有则使用 CAS 进行添加,添加失败进入下次循环
- 3、检查到内部在扩容,就帮助他一块扩容(Moved )
- 4、如果 Node 节点存在,则使用 synchronized 锁住头结点,
- 如果是链表结构就进行链表的添加操作
- 如果是红黑树结构就进行红黑树的添加操作
- 5、判断链表的长度是否大于 8,如果大于就去转为树结构
- 如果是链表结构就进行链表的添加操作